home
***
CD-ROM
|
disk
|
FTP
|
other
***
search
/
Celestin Apprentice 5
/
Apprentice-Release5.iso
/
Source Code
/
Libraries
/
Sherlock 2.0
/
Mac v2.0 docs
/
Text Docs
/
Mac Debugging .txt
next >
Wrap
Text File
|
1996-04-05
|
10KB
|
246 lines
Debugging With Sherlock
This chapter offers tips on how to find and correct bugs more effectively. C programs are
particularly challenging to debug because the C language does not limit what you can do. This
Chapter is based on parts of the book Debugging C, with the permission of Robert Ward.
Pointer Bugs
The key to learning how to debug C programs is recognizing, understanding and correcting
pointer bugs. Pointers pervade all of C and they give the C language much of its power and
flexibility. However, pointers are dangerous, as well as useful.
This section addresses the most common situations involving pointer bugs:
• Pointer bugs arise from uninitialized pointers, dangling pointers and incorrect use of arguments
to function.
• Pointer bugs often destroy the computer code. The code destroyed by pointer bugs may have
been the code you wrote, code comprising run-time functions such as printf(), or operating system
code.
• To the new C programmer, pointer bugs produce symptoms that look like hardware malfunctions
or compiler bugs. The experienced C programmer recognizes these same symptoms as clear
indications of the existence of pointer bugs.
Types of Pointer Bugs
Pointer bugs arise from uninitialized pointers, dangling pointers and incorrect use of arguments to
functions. An uninitialized pointer is simply a pointer variable which is used before it has been
given a value. For example,
error1()
{
char *p;
*p = 'a';
}
In this example, p does not point to any specified location at the time that it used to store the
character 'a'. The location in memory is not specified by the code, and the results are
unpredictable. Possible symptoms will be discussed later.
The second type of pointer bug is the dangling pointer. A dangling pointer refers to an area of
memory which is no longer being used for its original purpose. For example,
error2()
{
char *p, *malloc();
p = malloc(25);
free(p);
}
The call to malloc() makes p point to an area of memory containing room for 25 characters. The
call to free() deallocates the space and causes p to become a dangling pointer. However, there is no
bug in this code. Any time you deallocate memory you create a dangling pointer—bugs arise from
using dangling pointers rather than just creating them.
Again, the results of using a dangling pointer are unpredictable. They are not specified by the C
program. In this example, if the memory released by the call to free() is later reallocated, using p
will probably corrupt the newly allocated memory.
A third kind of pointer bug is the mistaken parameter bug. A good example is the following:
error3()
{
int i;
scanf("%d", i);
}
This will not work as expected. The second argument to scanf() should have been &i, not i. The
scanf() function expects a pointer to i and gets the value of i instead. Thus, the pointer that scanf()
expects is incorrect. Once again this creates a hard-to-find bug.
This kind of error can corrupt the system stack. This happens because the called routines assume
the stack has a different structure from the stack that was actually created by the caller. If the called
program alters any stack variable, it will be altering a part of the stack that may not actually
correspond to the location of the stack variable.
This kind of bug can be eliminated by using function prototyping.
Effects of Pointer Bugs
Pointer bugs can be devastating. Any pointer bug has the potential for destroying any part of
memory—executable code, library functions such as printf(), the system stack used to keep track
of procedure calls and returns or even the operating system itself. Exactly which part of memory
depends on the value the pointer had at the time your program was loaded.
Symptoms of Pointer Bugs
The symptoms of pointer bugs vary depending on just what kind of code or data area are
destroyed by the bug. Such symptoms can not be taken at face value. When confronted with
behavior such as described below, always think first of pointer bugs.
Symptom : Your program crashes inside a function which you have written and which you know
to be debugged.
Cause: A pointer bug has destroyed your carefully debugged function.
Symptom: A library function suddenly ceases to work correctly.
Cause: A pointer bug has destroyed the library function instead of your own code.
Symptom : A bug is solid, i.e., it manifests itself in the same way when you run the program
several times. However, the bug goes away when you insert print statements into the code to get
more information about it.
Cause: Inserting the print statements changed the location of various parts of your code. Before
the print statements were inserted, the pointer bug destroyed code that was still to be executed.
After inserting the print statements, the pointer bug destroyed a part of the program which was no
longer executed after the pointer bug occurred.
Symptom: The symptoms of a bug change after you insert print statements.
Cause: Inserting print statements changed the code destroyed by the pointer bug.
Symptom: By inserting print statements you can determine that control reaches a particular
statement but not the statement immediately following.
Cause: A pointer bug has destroyed one or both of the statements.
Symptom: Your program calls a function, but control never reaches the function.
Cause: A pointer bug has destroyed either the system stack or the function itself.
Symptom: Control never returns to the caller of a function after the called function returns.
Cause: A pointer bug has destroyed either the run-time stack or the function itself.
Symptom: Your program sometimes works and sometimes crashes.
Cause: An uninitialized variable is destroying random parts of your program depending on the
contents of certain memory locations before your program was invoked. The pointer bug is
destroying different memory locations each time your program is being run. Sometimes the
destroyed locations do not affect your program and sometimes they do.
Symptom: Your program always works once, but fails the second time it is run.
Cause: Your program contains an uninitialized variable. The first time your program runs the
variable is initialized in a uniform way which does not cause any apparent harm. The second time
your program runs, the variable has a new initial value which depends on the program’s first run.
This second initial value causes the symptoms the second time the program is run.
Using Sherlock to Find Bugs
Using Sherlock to locate bugs is a two-step process: 1) make the bug happen consistently and
2) make a plausible guess about its cause.
The first step in finding the cause of any bug is to make the bug “stand still” so that you can study
it. You can do this in the following ways:
1. Fill memory with a constant value before you run your program. This will insure that
uninitialized pointers get the same (though probably still incorrect) value from run to run. If filling
memory with a constant value makes the symptoms of your bug go away, you can be reasonably
sure that some kind of initialization problem is causing the bug.
2. Eliminate the effects (including symptoms) of most dangling pointers by disabling any routine
which frees a dynamically allocated data structure. You can do that by providing an alternative
deallocation routine which is a dummy routine. When you do that, dangling pointers no longer
dangle, i.e., they no longer point to deallocated memory. If disabling a routine such as free()
makes the symptoms of your bug go away, you can be reasonably sure that a dangling pointer is at
hand.
3. Keep precise records about how you invoked your program. An easy way to do this is by
invoking the program from a batch file, also known as a submit file or shell file. That way the
batch file serves as a record for exactly what you have done. A tip: when you change the
arguments to your program, comment out the old line and save it in the batch file as a record of the
your previous runs. This is especially handy when using numerous Sherlock tracepoints to
change the behavior of your program during testing.
4. Keep your program unchanged. Since pointer bugs destroy code, even the smallest change in
your program, or even a change in the order in which functions are linked together, may cause the
symptoms of pointer bugs to change or even go away. Sherlock is a huge help here. Enabling or
disabling different tracepoints does not change the location of any code in your program. You can
run test after test on your program, getting very different information each time, and the symptoms
of pointer bugs will not change.
The second step in finding a bug is to make a guess about what is causing it. If any of the
symptoms mentioned above appear, you probably should assume that a pointer bug is causing
your problems. Use Sherlock to look for bad pointers by tracing the parameters passed to all your
functions.
If a supposed pointer has a small negative number as its value (.e.g., FFFFE hex), you can be
sure that the pointer has already been corrupted. You may also notice that a pointer does not have
a reasonable value in some other way. Now ask yourself, “Which routines could have passed that
pointer on to the called routine?” Rerun your program with different tracepoints enabled which
will trace the likely culprits. Once you find the source of a bad pointer, ask whether the code
which created the bad pointer was ultimately at fault or whether some other error resulted in the
bad pointer as a by-product.
You will find that patterns jump out at you as you look at traces and dumps produced by Sherlock.
When you get a hint of something in a trace being not quite right, you can immediately start a new
trace to zero in on those parts of the program that are related to what caught your attention. By
varying your traces to home in on suspects, you are eliminating vast amounts of extraneous
information in the debugging output.